package	model.code;

import editor.ErrorHandler;
import event.*;
import lang.Language;
import model.code.line.*;
import model.symbol.*;
import observer.*;
import util.ASUtil;

import java.util.HashMap;

/**
 * This is an object to represent a function.
 *
 * @author Neil Dickson
 */
public class Function extends CodeFileChunk implements Symbol, ScopeObserver, ListObserver {
	private	ObservedList<Rowable>		seeAlso;

	/**
	 * True iff the function is exported as a library function
	 */
	private	ObservedBoolean				isExported;

	/**
	 * Holds the alignment if specified, or {@link CodeFileChunk#DEFAULT_ALIGNMENT_MARKER} if default
	 */
	private ObservedInt					alignment;
	/**
	 * Holds a string representing an expression that evaluates to the desired offset or empty if default
	 */
	private ValueLine					offset;
	/**
	 * Holds a string representing the type of scope that has access to this Function.
	 * NOTE: This is not necessarily the scope returned by scope.getParent(), because a global function can still access its own file's scope.
	 * Possible values are contained in {@link #ACCESS_SCOPE_TYPES}.
	 * "" represents whatever the default is (currently global).
	 */
	private	ObservedString				accessScopeType;
	/**
	 * Holds a string representing the calling convention of this Function.
	 * Possible values are contained in {@link #CALLING_CONVENTION_NAMES}.
	 * "" represents whatever the default is.
	 */
	private	ObservedString				callingConvention;

	private	ObservedList<LocalVariable>	parameters;
	private	ObservedList<LocalVariable>	localVariables;

	private	ObservedList<Return>		returns;

	private Scope						scope;

	private MultiLineList				code;

	private	boolean						isExisting;

	private	Object						testFunctionInfo;

	private static SingleLine.LineObserver	testNameObserver	= null;

	public static final String		NO_CALLING_CONVENTION_TEXT	= "";
	public static final String		INLINE_TEXT					= "inline";
	public static final String		C_TEXT						= "C";
	public static final String		FAR_TEXT					= "far";
	public static final String		STANDARD_TEXT				= "standard";
	public static final String		STANDARD64_TEXT				= "standard64";
	public static final String[]	CALLING_CONVENTION_NAMES	= new String[]{NO_CALLING_CONVENTION_TEXT,INLINE_TEXT,STANDARD64_TEXT,STANDARD_TEXT,C_TEXT,"fast","this",FAR_TEXT};

	/**
	 * In-editor type marker for a parameter passed by register not in the calling convention
	 */
	public static final String	AUTOMATIC_PARAM_TYPE_TEXT = "register";
	public static final String	REQUIRED_MACRO_PARAM_TEXT = "req";

	public Function(String name, String description, Language language) {
		super(name,description);
		seeAlso = new ObservedList<Rowable>();
		isExported = new ObservedBoolean(false);
		alignment = new ObservedInt(CodeFileChunk.DEFAULT_ALIGNMENT_MARKER);
		offset = new ValueLine(Line.EMPTY_CHAR_ARRAY,this,LOCATION_MODIFIERS,0);
		accessScopeType = new ObservedString();
		callingConvention = new ObservedString();
		parameters = new ObservedList<LocalVariable>();
		parameters.addObserver(this);
		localVariables = new ObservedList<LocalVariable>();
		localVariables.addObserver(this);
		returns = new ObservedList<Return>();
		scope = new Scope(Scope.TYPE_FUNCTION);
		scope.addObserver(this);
		code = new MultiLineList(this,LOCATION_CODE_PANEL,language);
		isExisting = false;
	}

	public Scope getScope() {
		return scope;
	}

	public ObservedList<Rowable> getSeeAlso() {
		return seeAlso;
	}

	public boolean isExported() {
		return isExported.get();
	}
	public boolean isImported() {
		return getFile()!=null && getFile().isExternal();
	}

	public static void setTestNameObserver(SingleLine.LineObserver testNameObserver) {
		Function.testNameObserver = testNameObserver;
	}
	public void setTestFunctionInfo(Object testFunctionInfo) {
		this.testFunctionInfo = testFunctionInfo;
	}
	public Object getTestFunctionInfo() {
		return testFunctionInfo;
	}

	public ObservedList<LocalVariable> getParameters() {
		return parameters;
	}

	public ParameterAddedEvent addParameter(LocalVariable parameter) {
		return addParameter(parameter,parameters.size());
	}
	public ParameterAddedEvent addParameter(final LocalVariable parameter,int index) {
		return addParameter(parameter,index,true);
	}
	private ParameterAddedEvent addParameter(final LocalVariable parameter,int index,boolean updateList) {
		if (updateList) {
			parameters.add(index,parameter,this);
		}
//		parameter.getName().addObserver(new StringObserver(){
//			public void valueChanged(ObservedString s, String oldValue) {
//				SymbolRenameEvent event = new SymbolRenameEvent(parameter,oldValue,s.toString());
//				scope.symbolChanged(event);
//			}
//		});
		parameter.getType().addObserver(new SingleLine.LineObserver(){
			public void valueChanged(SingleLine s, String oldValue) {
				SymbolTypeChangeEvent event = new SymbolTypeChangeEvent(parameter,oldValue,s.toString());
				scope.symbolChanged(event);
			}
		});
		scope.add(parameter);
		Scope globalScope = getGlobalScope();
		ParameterAddedEvent event = new ParameterAddedEvent(this,parameter,index);
		if (globalScope!=null && isExisting) {
			globalScope.symbolChanged(event);
		}
		return event;
	}
	public ParameterRemovedEvent removeParameter(int index) {
		return removeParameter(parameters.get(index),index,true);
	}
	private ParameterRemovedEvent removeParameter(LocalVariable parameter,int index,boolean updateList) {
		if (updateList) {
			parameters.remove(index,this);
		}
		// FIXME: Remove the StringObserver on the type
		scope.remove(parameter);
		Scope globalScope = getGlobalScope();
		ParameterRemovedEvent event = new ParameterRemovedEvent(this,parameter,index);
		if (globalScope!=null && isExisting) {
			globalScope.symbolChanged(event);
		}
		return event;
	}
	public ParameterMovedEvent moveParameter(int oldIndex,int newIndex) {
		return moveParameter(oldIndex,newIndex,true);
	}
	private ParameterMovedEvent moveParameter(int oldIndex,int newIndex,boolean updateList) {
		if (updateList) {
			parameters.move(oldIndex,newIndex,this);
		}
		Scope globalScope = getGlobalScope();
		ParameterMovedEvent event = new ParameterMovedEvent(this,oldIndex,newIndex);
		if (globalScope!=null && isExisting) {
			globalScope.symbolChanged(event);
		}
		return event;
	}

	public void addLocalVariable(LocalVariable localVariable) {
		addLocalVariable(localVariable,localVariables.size());
	}
	public void addLocalVariable(final LocalVariable localVariable,int index) {
		addLocalVariable(localVariable,index,true);
	}
	private void addLocalVariable(final LocalVariable localVariable,int index,boolean updateList) {
		if (updateList) {
			localVariables.add(index,localVariable,this);
		}
//		localVariable.getName().addObserver(new StringObserver(){
//			public void valueChanged(ObservedString s, String oldValue) {
//				SymbolRenameEvent event = new SymbolRenameEvent(localVariable,oldValue,s.toString());
//				scope.symbolChanged(event);
//			}
//		});
		localVariable.getType().addObserver(new SingleLine.LineObserver(){
			public void valueChanged(SingleLine s, String oldValue) {
				SymbolTypeChangeEvent event = new SymbolTypeChangeEvent(localVariable,oldValue,s.toString());
				scope.symbolChanged(event);
			}
		});
		scope.add(localVariable);
	}
	public void removeLocalVariable(int index) {
		removeLocalVariable(localVariables.get(index),index,true);
	}
	private void removeLocalVariable(LocalVariable localVariable,int index,boolean updateList) {
		if (updateList) {
			localVariables.remove(index,this);
		}
		// FIXME: Remove the StringObserver on the type
		scope.remove(localVariable);
	}
	public void moveLocalVariable(int oldIndex,int newIndex) {
		moveLocalVariable(oldIndex,newIndex,true);
	}
	private void moveLocalVariable(int oldIndex,int newIndex,boolean updateList) {
		if (updateList) {
			localVariables.move(oldIndex,newIndex,this);
		}
	}

	public ObservedList<LocalVariable> getLocalVariables() {
		return localVariables;
	}

	public MultiLineList getCode() {
		return code;
	}

	public void setSeeAlso(String[] seeAlso) {
		this.seeAlso = new ObservedList<Rowable>(seeAlso.length);
		for (int i = 0; i<seeAlso.length; i++) {
			String string = seeAlso[i];
			this.seeAlso.add(new Rowable(new SingleLine[]{new SingleLine(string.toCharArray(), this, LOCATION_SEE_ALSO, i<<16)}));
		}
	}

	public ObservedList<Return> getReturns() {
		return returns;
	}

	public ObservedBoolean getIsExported() {
		return isExported;
	}

	public ObservedInt getAlignment() {
		return alignment;
	}

	public ValueLine getOffset() {
		return offset;
	}

	public ObservedString getAccessScopeType() {
		return accessScopeType;
	}

	public ObservedString getCallingConvention() {
		return callingConvention;
	}

	public String toString() {
		return name.toString();
	}

	public String[] getParameterTypes() {
		String[] types = new String[parameters.size()];
		for (int i=0;i<parameters.size();++i) {
			types[i] = parameters.get(i).getType().toString();
		}
		return types;
	}

	public String[] getParameterNames() {
		String[] types = new String[parameters.size()];
		for (int i=0;i<parameters.size();++i) {
			types[i] = parameters.get(i).getName().toString();
		}
		return types;
	}

	/**
	 * Reassesses all {@link Line}s now that there is a global {@link Scope} and language {@link Scope} for this Function
	 */
	public void nowHaveGlobalScope() {
		isExisting = true;
		scope.setParent(getGlobalScope());
		for (LocalVariable param : parameters) {
			param.nowHaveGlobalScope();
		}
		for (LocalVariable var : localVariables) {
			var.nowHaveGlobalScope();
		}
		for (int i=0;i<code.size();++i) {
			String text = code.get(i).toString();
			code.get(i).replace(0,code.get(i).length(),"");
			code.get(i).replace(0,0,text);
			if ((i&0x3FFF)==0x3FFF) {
				System.out.println("Updating line "+(i+1)+"/"+code.size());
			}
		}
		if (!isImported()) {
			getGlobalScope().addObserver(this);
			if (testNameObserver!=null) {
				name.addObserver(testNameObserver);
				testNameObserver.valueChanged(name,name.toString());
			}
		}
	}

	public void symbolAdded(Scope scope, Symbol symbol) {
		boolean changed = false;
		HashMap<Register,DataType> newAssumptions = (code.size()==0 || !(code.get(0) instanceof ASMLine)) ? null : ((ASMLine)code.get(0)).getAssumptions();
		for (int i=0;i<code.size();++i) {
			Line line = code.get(i);
			if (line instanceof ASMLine) {
				ASMLine asmLine = (ASMLine)line;
				if (asmLine.isAssumeLine()) {
					changed |= asmLine.reassessAssumptionsOnAdd(newAssumptions,symbol);
					newAssumptions = asmLine.getAssumptions();
				}
				else {
					changed |= asmLine.reassessOnAdd(symbol);
				}
			}
		}
		if (changed) {
			code.notifyObservers();
		}
	}

	public void symbolRemoved(Scope scope, Symbol symbol) {
		boolean changed = false;
		HashMap<Register,DataType> newAssumptions = (code.size()==0 || !(code.get(0) instanceof ASMLine)) ? null : ((ASMLine)code.get(0)).getAssumptions();
		for (int i=0;i<code.size();++i) {
			Line line = code.get(i);
			if (line instanceof ASMLine) {
				ASMLine asmLine = (ASMLine)line;
				if (asmLine.isAssumeLine()) {
					changed |= asmLine.reassessAssumptionsOnRemove(newAssumptions,symbol);
					newAssumptions = asmLine.getAssumptions();
				}
				else {
					changed |= asmLine.reassessOnRemove(symbol);
				}
			}
		}
		if (changed) {
			code.notifyObservers();
		}
	}

	public void symbolChanged(Scope scope, SymbolChangeEvent e) {
		// FIXME: Observe Structures for changes in their members
		LineList list = getFirstLineList();
		while (list!=null) {
			if (list instanceof TypeLine) {
				((TypeLine)list).reparse(e instanceof SymbolRenameEvent);
			}
			else if (list instanceof ValueLine) {
				((ValueLine)list).reparse(e instanceof SymbolRenameEvent);
			}
			list = getNextLineList(list);
		}
		boolean changed = false;
		HashMap<Register,DataType> newAssumptions = (code.size()==0) ? null : ((ASMLine)code.get(0)).getAssumptions();
		for (int i=0;i<code.size();++i) {
			Line line = code.get(i);
			if (line instanceof ASMLine) {
				ASMLine asmLine = (ASMLine)line;
				if (asmLine.isAssumeLine()) {
					changed |= asmLine.reassessAssumptionsOnChange(newAssumptions,e);
					newAssumptions = asmLine.getAssumptions();
				}
				else {
					changed |= asmLine.reassessOnChange(e);
				}
			}
		}
		if (changed) {
			code.notifyObservers();
		}
	}

	public LineList getLineList(int locType, int locIndex) {
		int row = locIndex>>>16;
		int col = locIndex&0xFFFF;
		ObservedList<? extends Rowable> table = null;
		if (locType==LOCATION_CODE_PANEL) {
			return code;
		}
		if (locType==LOCATION_PARAMETERS) {
			table = parameters;
		}
		else if (locType==LOCATION_LOCAL_VARS) {
			table = localVariables;
		}
		else if (locType==LOCATION_RETURNS) {
			table = returns;
		}
		else if (locType==LOCATION_SEE_ALSO) {
			table = seeAlso;
		}
		else if (locType==LOCATION_MODIFIERS) {
			// NOTE: Update this if there are more modifier LineLists added.
			return offset;
		}
		else {
			return super.getLineList(locType, locIndex);
		}
		if (table!=null && table.size()>row && table.get(row).getStrings().length>col) {
			return table.get(row).getStrings()[col];
		}
		return null;
	}

	public LineList getNextLineList(LineList list) {
		int locType = list.getLocationType();
		int locIndex = list.getLocationIndex();
		if (locType==LOCATION_NAME) {
			return description;
		}
		if (locType==LOCATION_DESCRIPTION) {
			if (parameters.size()>0) {
				return getLineList(LOCATION_PARAMETERS,0);
			}
			if (localVariables.size()>0) {
				return getLineList(LOCATION_LOCAL_VARS,0);
			}
			if (returns.size()>0) {
				return getLineList(LOCATION_RETURNS,0);
			}
			return getLineList(LOCATION_MODIFIERS,0);
		}
		if (locType==LOCATION_MODIFIERS) {
			if (seeAlso.size()>0) {
				return getLineList(LOCATION_SEE_ALSO,0);
			}
			return code;
		}
		ObservedList<? extends Rowable> table = null;
		int row = locIndex>>>16;
		int col = locIndex&0xFFFF;
		if (locType==LOCATION_PARAMETERS) {
			table = parameters;
		}
		if (locType==LOCATION_LOCAL_VARS) {
			table = localVariables;
		}
		if (locType==LOCATION_RETURNS) {
			table = returns;
		}
		if (locType==LOCATION_SEE_ALSO) {
			table = seeAlso;
		}
		if (table!=null) {
			if (col+1<table.get(row).getSize()) {
				return getLineList(locType,locIndex+1);
			}
			if (row+1<table.size()) {
				return getLineList(locType,(row+1)<<16);
			}
			if (locType==LOCATION_PARAMETERS && localVariables.size()>0) {
				return getLineList(LOCATION_LOCAL_VARS,0);
			}
			if ((locType==LOCATION_PARAMETERS || locType==LOCATION_LOCAL_VARS) && returns.size()>0) {
				return getLineList(LOCATION_RETURNS,0);
			}
			if (locType==LOCATION_PARAMETERS || locType==LOCATION_LOCAL_VARS || locType==LOCATION_RETURNS) {
				return getLineList(LOCATION_MODIFIERS,0);
			}
			return code;
		}
		return null;
	}
	public LineList getPrevLineList(LineList list) {
		int locType = list.getLocationType();
		int locIndex = list.getLocationIndex();
		if (locType==LOCATION_NAME) {
			return null;
		}
		if (locType==LOCATION_DESCRIPTION) {
			return name;
		}
		if (locType==LOCATION_MODIFIERS) {
			if (returns.size()>0) {
				return returns.getLast().getStrings()[returns.getLast().getSize()-1];
			}
			if (localVariables.size()>0) {
				return localVariables.getLast().getStrings()[localVariables.getLast().getSize()-1];
			}
			if (parameters.size()>0) {
				return parameters.getLast().getStrings()[parameters.getLast().getSize()-1];
			}
			return description;
		}
		if (locType==LOCATION_CODE_PANEL) {
			if (seeAlso.size()>0) {
				return seeAlso.getLast().getStrings()[seeAlso.getLast().getSize()-1];
			}
			return getLineList(LOCATION_MODIFIERS,0);
		}
		ObservedList<? extends Rowable> table = null;
		int row = locIndex>>>16;
		int col = locIndex&0xFFFF;
		if (locType==LOCATION_PARAMETERS) {
			table = parameters;
		}
		if (locType==LOCATION_LOCAL_VARS) {
			table = localVariables;
		}
		if (locType==LOCATION_RETURNS) {
			table = returns;
		}
		if (locType==LOCATION_SEE_ALSO) {
			table = seeAlso;
		}
		if (table!=null) {
			if (col-1>=0) {
				return getLineList(locType,locIndex-1);
			}
			if (row-1>=0) {
				return getLineList(locType,(row-1)<<16);
			}
			if (locType==LOCATION_SEE_ALSO) {
				return getLineList(LOCATION_MODIFIERS,0);
			}
			if (locType==LOCATION_RETURNS && localVariables.size()>0) {
				return localVariables.getLast().getStrings()[localVariables.getLast().getSize()-1];
			}
			if ((locType==LOCATION_RETURNS || locType==LOCATION_LOCAL_VARS) && parameters.size()>0) {
				return parameters.getLast().getStrings()[parameters.getLast().getSize()-1];
			}
			return description;
		}
		return null;
	}

	public LineList getLastLineList() {
		return code;
	}

	public void listElementAdded(ObservedList s, int index) {
		if (s==parameters) {
			addParameter(parameters.get(index),index,false);
		}
		else if (s==localVariables) {
			addLocalVariable(localVariables.get(index),index,false);
		}
	}

	public void listElementRemoved(ObservedList s, int index, Copiable element) {
		if (s==parameters) {
			removeParameter((LocalVariable)element,index,false);
		}
		else if (s==localVariables) {
			removeLocalVariable((LocalVariable)element,index,false);
		}
	}

	public void listElementSet(ObservedList s, int index, Copiable oldElement) {
		if (s==parameters) {
			ErrorHandler.showNotImplementedMessage("Sorry, there's an unimplemented function named Function.listElementSet, and it was called, but it shouldn't have been.  The Function was being notified that a parameter was set.  Hopefully this bug gets fixed in a later version.");
		}
		else if (s==localVariables) {
			ErrorHandler.showNotImplementedMessage("Sorry, there's an unimplemented function named Function.listElementSet, and it was called, but it shouldn't have been.  The Function was being notified that a LocalVariable was set.  Hopefully this bug gets fixed in a later version.");
		}
	}

	public void listElementMoved(ObservedList s, int from, int to) {
		if (s==parameters) {
			moveParameter(from,to,false);
		}
		else if (s==localVariables) {
			moveLocalVariable(from,to,false);
		}
	}

	public Function copy() {
		return copy(null);
	}
	public Function copy(Section parent) {
		Function that = new Function(getName().toString(),getDescription().toString(),getLanguage());
		if (parent!=null) {
			that.setParent(parent);
			that.scope.setParent(parent.getScope());
		}
		that.seeAlso.ensureCapacity(seeAlso.size());
		for (Rowable sa : seeAlso) {
			that.seeAlso.add(sa.copy(that,that.seeAlso.size()));
		}
		that.isExported = isExported.copy();
		that.alignment = alignment.copy();
		that.offset = offset.copy(that);
		that.accessScopeType = accessScopeType.copy();
		that.callingConvention = callingConvention.copy();
		that.parameters.ensureCapacity(parameters.size());
		for (LocalVariable var : parameters) {
			that.parameters.add(var.copy(that,that.parameters.size()));
		}
		// NOTE: If scope notification isn't disabled here, the addAll() will crash because there's a SimpleLine in that.code, not a code line
		that.scope.setNotificationEnabled(false);
		that.scope.addAll(that.parameters);
		that.localVariables.ensureCapacity(localVariables.size());
		for (LocalVariable var : localVariables) {
			that.localVariables.add(var.copy(that,that.localVariables.size()));
		}
		that.scope.addAll(that.localVariables);
		that.returns.ensureCapacity(returns.size());
		for (Return ret : returns) {
			that.returns.add(ret.copy(that,that.returns.size()));
		}
		that.code = code.copy(that);
		that.scope.setNotificationEnabled(true);
		// For purposes of encoding the inline test helper functions, local symbols that are really GlobalVariables need to remain in the local scope, since they're not in global scope.
		for (Symbol symbol : scope.getAllLocal()) {
			if (symbol instanceof GlobalVariable) {
				that.scope.add(symbol);
			}
		}
		return that;
	}

	public static class Return extends Rowable {
		/**
		 * This is because Java can't do virtual static functions
		 */
		public static final Return	DUMMY = new Return(Line.EMPTY_CHAR_ARRAY,Line.EMPTY_CHAR_ARRAY,null,0);

		public static final int	TYPE_INDEX			= 0;
		public static final int	DESCRIPTION_INDEX	= 1;
		public static final int	NUM_STIRNGS			= 2;

		public Return(char[] type,char[] description,CodeFileChunk parent,int row) {
			super(new SingleLine[]{new TypeLine(type,parent,CodeFileChunk.LOCATION_RETURNS,(row<<16)+TYPE_INDEX)
					,new SingleLine(description,parent,CodeFileChunk.LOCATION_RETURNS,(row<<16)+DESCRIPTION_INDEX)});
		}
		public SingleLine getType() {
			return strings[TYPE_INDEX];
		}
		public SingleLine getDescription() {
			return strings[DESCRIPTION_INDEX];
		}
		/**
		 * This should be overridden by all subclasses.  The subclasses must return an object of their own class.
		 * @return a new Rowable that is a copy of this one
		 */
		public Return copy(CodeFileChunk parent,int row) {
			SingleLine[] newStrings = new SingleLine[strings.length];
			for (int i=0;i<strings.length;++i) {
				newStrings[i] = new SingleLine(ASUtil.subCharArray(strings[i].getText(),0,strings[i].length()),parent,(row<<16)+i);
			}
			return new Return(getType().toCharArray(),getDescription().toCharArray(),parent,row);
		}
	}
}
